﻿using System.Windows.Input;
using System.Reflection;
using System;
using System.Linq;
using System.ComponentModel;

namespace ProxyViewModel.Commands {
	public class ProxyCommand : ICommand {
		private MethodInfo _executeMethod;
		private MethodInfo _canExecuteMethod;
		private readonly object _viewModel;
		private readonly string _executeMethodName;

		public ProxyCommand(object viewModel, string executeMethodName) {
			_viewModel = viewModel;
			if (!(_viewModel is IProxyCommandsContainer)) {
				// todo: custom exception here...
				throw new Exception("ProxyCommand can only be used on a ViewModel that implements IProxyCommandsContainer");
			}
			(_viewModel as IProxyCommandsContainer).Commands.Add(this);
			_executeMethodName = executeMethodName;

#if !SILVERLIGHT
			CommandManager.RequerySuggested += new EventHandler(CommandManager_RequerySuggested);
#endif
		}

		private void CommandManager_RequerySuggested(object sender, EventArgs e) {
			if (CanExecuteChanged != null) {
				CanExecuteChanged(this, e);
			}
		}

		public void Invalidate() {
			if (CanExecuteChanged != null) {
				CanExecuteChanged(this, EventArgs.Empty);
			}
		}

		private void DelayInitialize() {
			if (_executeMethod == null) {
				_executeMethod = ReflectionHelper.GetMethodByName(_viewModel.GetType(), _executeMethodName);
				_canExecuteMethod = GetCanExecuteCommand(_executeMethod, _viewModel.GetType());
			}
		}

		private MethodInfo GetCanExecuteCommand(MethodInfo executeMethod, Type operatable) {
			string canExecuteMethodName = string.Format("CanExecute{0}Command", executeMethod.Name.Replace("Command", string.Empty));
			MethodInfo canExecute = ReflectionHelper.GetMethodByName(operatable, canExecuteMethodName);
			if (canExecute != null && !ReflectionHelper.MethodSignatureEquals(executeMethod, canExecute)) {
				throw new Exception(string.Format("'{0}' method should have the same signature as '{1}'. Because it is considered to be a predicate of '{1}' command",
					executeMethod.Name, canExecuteMethodName));
			}
			return canExecute;
		}

		#region ICommand Members

		public bool CanExecute(object parameter) {
			DelayInitialize();
			bool canExecute = true;

			if (_canExecuteMethod != null) {
				if (_canExecuteMethod.GetParameters().Length > 0) {
					try {
						canExecute = (bool)_canExecuteMethod.Invoke(_viewModel, new object[] { parameter });
					}
					catch (Exception ex) {
						throw new Exception(string.Format("Could not invoke {0} method with parameter of type {1}. See inner exception",
						_executeMethod.Name, parameter.GetType()), ex);
					}
				}
				else {
					canExecute = (bool)_canExecuteMethod.Invoke(_viewModel, null);
				}
			}

			return canExecute;
		}

		public void Execute(object parameter) {
			DelayInitialize();
			if (_executeMethod.GetParameters().Length > 0) {
				try {
					_executeMethod.Invoke(_viewModel, new object[] { parameter });
				}
				catch (Exception ex) {
					throw new Exception(string.Format("Could not invoke {0} method with parameter of type {1}. See inner exception",
						_executeMethod.Name, parameter.GetType()), ex);
				}
			}
			else {
				_executeMethod.Invoke(_viewModel, null);
			}
		}

		public event EventHandler CanExecuteChanged;

		#endregion
	}
}
